﻿/*
 *  Copyright 2010 David Hawkes, daveh@cadlink.com
 *  Subject to the Apache License version 2.0 at http://www.apache.org/licenses/
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using Microsoft.CSharp.RuntimeBinder;

namespace DynamicAccess
{
  /// <summary>
  /// static class that holds dynamic accessors
  /// </summary>
  public static class Accessors
  {
    /// <summary>
    /// Get the dynamic targets member
    /// </summary>
    /// <param name="target">dynamic target</param>
    /// <param name="name">member name</param>
    /// <returns>targets member</returns>
    public static object GetMember(object target, string name)
    {
      try {
        return Expression.Lambda(
          Expression.Dynamic(
            Microsoft.CSharp.RuntimeBinder.Binder.GetMember(
              Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags.None,
              name,
              target.GetType(),
              new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }
            ),
            typeof(object),
            new[] { Expression.Constant(target) }
          )
        ).Compile().DynamicInvoke();
      } catch(Exception e) {
        throw new AccessorsException("GetMember Failure", e);
      }
    }

    /// <summary>
    /// Set the dynamic targets member
    /// </summary>
    /// <param name="target">dynamic target</param>
    /// <param name="name">member name</param>
    /// <param name="val">value to set</param>
    public static void SetMember(object target, string name, object val)
    {
      try {
        Expression.Lambda(
          Expression.Dynamic(
            Microsoft.CSharp.RuntimeBinder.Binder.SetMember(
              Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags.ResultDiscarded,
              name,
              target.GetType(),
              new[] { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null), CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) }
            ),
            typeof(object),
            new[] { Expression.Constant(target), Expression.Constant(val) }
          )
        ).Compile().DynamicInvoke();
      } catch(Exception e) {
        throw new AccessorsException("SetMember Failure", e);
      }
    }

    /// <summary>
    /// Deletes the dynamic targets member
    /// </summary>
    /// <param name="target">dynamic target</param>
    /// <param name="name">member name</param>
    public static void DeleteMember(object target, string name)
    {
      try {
        Expression.Lambda(
          Expression.Dynamic(
            new DynExDeleteMemberBinder(name),
            typeof(void),
            new[] { Expression.Constant(target) }
          )
        ).Compile().DynamicInvoke();
      } catch(Exception e) {
        throw new AccessorsException("DeleteMember Failure", e);
      }
    }

    /// <summary>
    /// Get the dynamic targets index
    /// </summary>
    /// <param name="target">dynamic target</param>
    /// <param name="args">index arguments</param>
    /// <returns>targets index value</returns>
    public static object GetIndex(object target, params object[] args)
    {
      try {
        if(target.GetType().IsArray) {
          var indexes = new List<Expression>();
          foreach(var arg in args)
            indexes.Add(Expression.Constant(arg));
          return Expression.Lambda(
            Expression.ArrayAccess(
              Expression.Constant(target),
              indexes
            )
          ).Compile().DynamicInvoke();
        } else {
          var argInfo = new List<CSharpArgumentInfo>() { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) };
          foreach(var arg in args)
            argInfo.Add(CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null));
          return GenericCall(
            Microsoft.CSharp.RuntimeBinder.Binder.GetIndex(
              Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags.None,
              target.GetType(),
              argInfo
            ),
            target,
            args
          );
        }
      } catch(Exception e) {
        throw new AccessorsException("GetIndex Failure", e);
      }
    }

    /// <summary>
    /// Set the dynamic targets index
    /// </summary>
    /// <param name="target">dynamic target</param>
    /// <param name="args">index arguments and value to set is last argument</param>
    public static void SetIndex(object target, params object[] args)
    {
      try {
        if(target.GetType().IsArray) {
          var indexes = new List<Expression>();
          var argsLast = args.Length - 1;
          for(var i = 0; i < argsLast; ++i)
            indexes.Add(Expression.Constant(args[i]));
          Expression.Lambda(
            Expression.Assign(
              Expression.ArrayAccess(
                Expression.Constant(target),
                indexes
              ),
              Expression.Constant(args[argsLast])
            )
          ).Compile().DynamicInvoke();
        } else {
          var argInfo = new List<CSharpArgumentInfo>() { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) };
          foreach(var arg in args)
            argInfo.Add(CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null));
          GenericCall(
            Microsoft.CSharp.RuntimeBinder.Binder.SetIndex(
              Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags.ResultDiscarded,
              target.GetType(),
              argInfo
            ),
            target,
            args
          );
        }
      } catch(Exception e) {
        throw new AccessorsException("SetIndex Failure", e);
      }
    }

    /// <summary>
    /// Deletes the dynamic targets index
    /// </summary>
    /// <param name="target">dynamic target</param>
    /// <param name="args">call arguments</param>
    /// <returns>nothing</returns>
    public static void DeleteIndex(object target, params object[] args)
    {
      try {
        var exps = new List<Expression> { Expression.Constant(target) };
        foreach(var arg in args)
          exps.Add(Expression.Constant(arg));
        Expression.Lambda(
          Expression.Dynamic(
            new DynExDeleteIndexBinder(new CallInfo(args.Length)),
            typeof(void),
            exps
          )
        ).Compile().DynamicInvoke();
      } catch(Exception e) {
        throw new AccessorsException("DeleteIndex Failure", e);
      }
    }

    /// <summary>
    /// Call the dynamic target member
    /// </summary>
    /// <param name="target">dynamic target</param>
    /// <param name="name">member name</param>
    /// <param name="args">call arguments</param>
    /// <returns>call return value</returns>
    public static object CallMember(object target, string name, params object[] args)
    {
      try {
        var argTypes = new List<Type>();
        var argInfo = new List<CSharpArgumentInfo> { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) };
        foreach(var arg in args) {
          argTypes.Add(arg.GetType());
          argInfo.Add(CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null));
        }
        return GenericCall(
          Microsoft.CSharp.RuntimeBinder.Binder.InvokeMember(
            Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags.InvokeSimpleName,
            name,
            argTypes,
            target.GetType(),
            argInfo
          ),
          target,
          args
        );
      } catch(Exception e) {
        throw new AccessorsException("CallMember Failure", e);
      }
    }

    /// <summary>
    /// Call the dynamic target
    /// </summary>
    /// <param name="target">dynamic target</param>
    /// <param name="args">call arguments</param>
    /// <returns>call return value</returns>
    public static object Call(object target, params object[] args)
    {
      try {
        var argInfo = new List<CSharpArgumentInfo>() { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) };
        foreach(var arg in args)
          argInfo.Add(CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null));
        return GenericCall(
          Microsoft.CSharp.RuntimeBinder.Binder.Invoke(
            Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags.InvokeSimpleName,
            target.GetType(),
            argInfo
          ),
          target,
          args
        );
      } catch(Exception e) {
        throw new AccessorsException("Call Failure", e);
      }
    }

    /// <summary>
    /// Create an instance from the dynamic target
    /// </summary>
    /// <param name="target">dynamic target</param>
    /// <param name="args">constructor arguments</param>
    /// <returns>created instance</returns>
    public static object CreateInstance(object target, params object[] args)
    {
      try {
        var binder = new DynExCreateInstanceBinder(new CallInfo(args.Length));
        return GenericCall(binder, target, args);
      } catch(Exception e) {
        throw new AccessorsException("CreateInstance Failure", e);
      }
    }

    private static object GenericCall(CallSiteBinder binder, object target, params object[] args)
    {
      var exps = new List<Expression> { Expression.Constant(target, typeof(object)) };
      var parms = new List<ParameterExpression>(); // { Expression.Parameter(target.GetType()) };
      foreach(var arg in args) {
        exps.Add(Expression.Constant(arg));
        parms.Add(Expression.Parameter(arg.GetType()));
      }
      return Expression.Lambda(
        Expression.Dynamic(
          binder,
          typeof(object),
          exps
        ),
        parms
      ).Compile().DynamicInvoke(args);
    }
  }

  [Serializable]
  internal class AccessorsException : Exception
  {
    internal AccessorsException(string message, Exception innerException) : base(message, innerException) { }
    internal static Expression CreateThrowExpression(string message, Exception innerException)
    {
      return Expression.Throw(
        Expression.New(
          typeof(AccessorsException).GetConstructor(new[] { typeof(string), typeof(Exception) }),
          new[] { Expression.Constant(message, typeof(string)), Expression.Constant(innerException, typeof(Exception)) }
        ),
        typeof(AccessorsException)
      );
    }
  }

  internal class DynExDeleteMemberBinder : DeleteMemberBinder
  {
    public DynExDeleteMemberBinder(string name)
      : base(name, false)
    {
    }
    public override DynamicMetaObject FallbackDeleteMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion)
    {
      var helper = MemberTypeHelper.Create(target);
      if(helper == null) {
        if(errorSuggestion == null)
          return new DynamicMetaObject(AccessorsException.CreateThrowExpression("DeleteMember Failure", null), target.Restrictions);
        else
          return errorSuggestion;
      }
      return new DynamicMetaObject(helper.Delete(Name), target.Restrictions);
    }
  }

  internal class DynExDeleteIndexBinder : DeleteIndexBinder
  {
    public DynExDeleteIndexBinder(CallInfo callInfo)
      : base(callInfo)
    {
    }
    public override DynamicMetaObject FallbackDeleteIndex(DynamicMetaObject target, DynamicMetaObject[] indexes, DynamicMetaObject errorSuggestion)
    {
      var helper = IndexTypeHelper.Create(target);
      if(helper == null) {
        if(errorSuggestion == null)
          return new DynamicMetaObject(AccessorsException.CreateThrowExpression("DeleteIndex Failure", null), target.Restrictions);
        else
          return errorSuggestion;
      }
      return new DynamicMetaObject(helper.Delete(indexes), target.Restrictions);
    }
  }

  internal class DynExCreateInstanceBinder : CreateInstanceBinder
  {
    public DynExCreateInstanceBinder(CallInfo callInfo)
      : base(callInfo)
    {
    }
    public override DynamicMetaObject FallbackCreateInstance(DynamicMetaObject target, DynamicMetaObject[] args, DynamicMetaObject errorSuggestion)
    {
      var exps = new List<Expression> { target.Expression };
      var argInfo = new List<CSharpArgumentInfo>() { CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null) };
      foreach(var arg in args) {
        argInfo.Add(CSharpArgumentInfo.Create(CSharpArgumentInfoFlags.None, null));
        exps.Add(arg.Expression);
      }
      return new DynamicMetaObject(
        Expression.Dynamic(
          Microsoft.CSharp.RuntimeBinder.Binder.Invoke(
            Microsoft.CSharp.RuntimeBinder.CSharpBinderFlags.InvokeSimpleName,
            target.GetType(),
            argInfo
          ),
          ReturnType,
          exps
        ),
        target.Restrictions
      );
    }
  }

  internal abstract class MemberTypeHelper
  {
    public static MemberTypeHelper Create(DynamicMetaObject target)
    {
      if(!target.HasValue)
        return null;
      if(target.Value.GetType() == typeof(ExpandoObject)) {
        var dict = target.Value as IDictionary<string, object>;
        if(dict != null)
          return new MemberTypeHelperDict<IDictionary<string, object>>(dict);
      }
      // We may want to recognise additional special types here
      /* var dict1 = target.Value as IDictionary;
      if(dict1 != null)
        return new MemberTypeHelperDict<IDictionary>(dict1);
      var dict2 = target.Value as IDictionary<string, object>;
      if(dict2 != null)
        return new MemberTypeHelperDict<IDictionary<string, object>>(dict2); */
      return null;
    }
    public abstract Expression Get(string name);
    public abstract Expression Set(string name, dynamic value);
    public abstract Expression Delete(string name);
  }

  internal class MemberTypeHelperDict<T> : MemberTypeHelper
  {
    internal MemberTypeHelperDict(T dict)
    {
      _dict = dict;
    }
    public override Expression Get(string name)
    {
      var resultParam = Expression.Parameter(typeof(object), "result");
      var methodInfo = typeof(T).GetMethod("TryGetValue");
      if(methodInfo != null) {
        return Expression.Block(
          new[] { resultParam },
          Expression.IfThen(
            Expression.IsFalse(
              Expression.Call(
                Expression.Constant(_dict),
                methodInfo,
                Expression.Constant(name),
                resultParam
              )
            ),
            AccessorsException.CreateThrowExpression("GetMember Failure", null)
          ),
          resultParam
        );
      } else {
        return Expression.Block(
          new[] { resultParam },
          Expression.TryCatch(
            Expression.Block(
              Expression.Assign(
                resultParam,
                Expression.Call(
                  Expression.Constant(_dict),
                  typeof(T).GetMethod("get_Item"),
                  Expression.Constant(name)
                )
              ),
              Expression.Constant(null)
            ),
            Expression.Catch(
              typeof(Exception),
              Expression.Block(
                AccessorsException.CreateThrowExpression("GetMember Failure", null),
                Expression.Constant(null)
              )
            )
          ),
          resultParam
        );
      }
    }
    public override Expression Set(string name, dynamic value)
    {
      var valueExpression = Expression.Constant(value);
      return Expression.Block(
        Expression.Call(
          Expression.Constant(_dict),
          typeof(T).GetMethod("set_Item"),
          Expression.Constant(name),
          valueExpression
        ),
        valueExpression
      );
    }
    public override Expression Delete(string name)
    {
      return Expression.Call(
        Expression.Constant(_dict),
        typeof(T).GetMethod("Remove"),
        Expression.Constant(name)
      );
    }
    T _dict;
  }
  internal abstract class IndexTypeHelper
  {
    public static IndexTypeHelper Create(DynamicMetaObject target)
    {
      if(!target.HasValue)
        return null;
      var TypeName = target.Value.GetType().FullName;
      if((TypeName == "IronPython.Runtime.PythonDictionary") || (TypeName == "IronRuby.Builtins.Hash")) {
        var dict1 = target.Value as IDictionary;
        if(dict1 != null)
          return new IndexTypeHelperDict<IDictionary>(dict1);
        var dict2 = target.Value as IDictionary<string, object>;
        if(dict2 != null)
          return new IndexTypeHelperDict<IDictionary<string, object>>(dict2);
      }
      // We may want to recognise additional special types here
      return null;
    }
    public abstract Expression Get(DynamicMetaObject[] indexes);
    public abstract Expression Set(DynamicMetaObject[] indexes, dynamic value);
    public abstract Expression Delete(DynamicMetaObject[] indexes);
  }
  internal class IndexTypeHelperDict<T> : IndexTypeHelper
  {
    public IndexTypeHelperDict(T dict)
    {
      _dict = dict;
    }
    public override Expression Get(DynamicMetaObject[] indexes)
    {
      throw new NotImplementedException();
    }
    public override Expression Set(DynamicMetaObject[] indexes, dynamic value)
    {
      throw new NotImplementedException();
    }
    public override Expression Delete(DynamicMetaObject[] indexes)
    {
      var exps = new List<Expression>();
      foreach(var index in indexes) {
        exps.Add(index.Expression);
      }
      return Expression.Call(
        Expression.Constant(_dict),
        typeof(T).GetMethod("Remove"),
        exps
      );
    }
    private T _dict;
  }
}
